/*
 * Copyright 2013-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.cloud.nacos;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.cloud.nacos.event.NacosDiscoveryInfoChangedEvent;
import com.alibaba.cloud.nacos.util.InetIPv6Utils;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.PreservedMetadataKeys;
import com.alibaba.nacos.client.naming.utils.UtilAndComs;
import com.alibaba.spring.util.PropertySourcesUtils;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.commons.util.InetUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;

import static com.alibaba.nacos.api.PropertyKeyConst.ACCESS_KEY;
import static com.alibaba.nacos.api.PropertyKeyConst.ENDPOINT;
import static com.alibaba.nacos.api.PropertyKeyConst.ENDPOINT_PORT;
import static com.alibaba.nacos.api.PropertyKeyConst.NAMESPACE;
import static com.alibaba.nacos.api.PropertyKeyConst.NAMING_LOAD_CACHE_AT_START;
import static com.alibaba.nacos.api.PropertyKeyConst.PASSWORD;
import static com.alibaba.nacos.api.PropertyKeyConst.SECRET_KEY;
import static com.alibaba.nacos.api.PropertyKeyConst.SERVER_ADDR;
import static com.alibaba.nacos.api.PropertyKeyConst.USERNAME;

/**
 * @author dungu.zpf
 * @author xiaojing
 * @author <a href="mailto:mercyblitz@gmail.com">Mercy</a>
 * @author <a href="mailto:lyuzb@lyuzb.com">lyuzb</a>
 * @author <a href="mailto:78552423@qq.com">eshun</a>
 * @author freeman
 */
@ConfigurationProperties("spring.cloud.nacos.discovery")
public class NacosDiscoveryProperties {

    /**
     * Prefix of {@link NacosDiscoveryProperties}.
     */
    public static final String PREFIX = "spring.cloud.nacos.discovery";
    private static final Logger log = LoggerFactory.getLogger(NacosDiscoveryProperties.class);
    private static final Pattern PATTERN = Pattern.compile("-(\\w)");

    private static final String IPV4 = "IPv4";

    private static final String IPV6 = "IPv6";

    /**
     * nacos discovery server address.
     */
    private String serverAddr;

    /**
     * the nacos authentication username.
     */
    private String username;

    /**
     * the nacos authentication password.
     */
    private String password;

    /**
     * the domain name of a service, through which the server address can be dynamically
     * obtained.
     */
    private String endpoint;

    /**
     * namespace, separation registry of different environments.
     */
    private String namespace;

    /**
     * watch delay,duration to pull new service from nacos server.
     */
    private long watchDelay = 30000;

    /**
     * nacos naming log file name.
     */
    private String logName;

    /**
     * service name to registry.
     */
    @Value("${spring.cloud.nacos.discovery.service:${spring.application.name:}}")
    private String service;

    /**
     * weight for service instance, the larger the value, the larger the weight.
     */
    private float weight = 1;

    /**
     * cluster name for nacos .
     */
    private String clusterName;

    /**
     * group name for nacos.
     */
    private String group = "DEFAULT_GROUP";

    /**
     * naming load from local cache at application start. true is load.
     */
    private String namingLoadCacheAtStart = "false";

    /**
     * extra metadata to register.
     */
    private Map<String, String> metadata = new HashMap<>();

    /**
     * if you just want to subscribe, but don't want to register your service, set it to
     * false.
     */
    private boolean registerEnabled = true;

    /**
     * The ip address your want to register for your service instance, needn't to set it
     * if the auto detect ip works well.
     */
    private String ip;

    /**
     * which network interface's ip you want to register.
     */
    private String networkInterface = "";

    /**
     * choose IPv4 or IPv6,if you don't set it will choose IPv4.
     * When IPv6 is chosen but no IPv6 can be found, system will automatically find IPv4 to ensure there is an
     * available service address.
     */
    private String ipType;

    /**
     * The port your want to register for your service instance, needn't to set it if the
     * auto detect port works well.
     */
    private int port = -1;

    /**
     * whether your service is a https service.
     */
    private boolean secure = false;

    /**
     * access key for namespace.
     */
    private String accessKey;

    /**
     * secret key for namespace.
     */
    private String secretKey;

    /**
     * Heart beat interval. Time unit: millisecond.
     */
    private Integer heartBeatInterval;

    /**
     * Heart beat timeout. Time unit: millisecond.
     */
    private Integer heartBeatTimeout;

    /**
     * Ip delete timeout. Time unit: millisecond.
     */
    private Integer ipDeleteTimeout;

    /**
     * If instance is enabled to accept request. The default value is true.
     */
    private boolean instanceEnabled = true;

    /**
     * If instance is ephemeral.The default value is true.
     */
    private boolean ephemeral = true;

    /**
     * Whether to enable nacos failure tolerance. If enabled, nacos will return cached
     * values when exceptions occur.
     */
    private boolean failureToleranceEnabled;

    /**
     * Throw exceptions during service registration if true, otherwise, log error
     * (defaults to true).
     */
    private boolean failFast = true;

    @Autowired
    private InetIPv6Utils inetIPv6Utils;

    @Autowired
    private InetUtils inetUtils;

    @Autowired
    private Environment environment;

    @Autowired
    private NacosServiceManager nacosServiceManager;

    @Autowired
    private ApplicationEventPublisher applicationEventPublisher;

    @PostConstruct
    public void init() throws Exception {

        metadata.put(PreservedMetadataKeys.REGISTER_SOURCE, "SPRING_CLOUD");
        if (secure) {
            metadata.put("secure", "true");
        }

        serverAddr = Objects.toString(serverAddr, "");
        if (serverAddr.endsWith("/")) {
            serverAddr = serverAddr.substring(0, serverAddr.length() - 1);
        }
        endpoint = Objects.toString(endpoint, "");
        namespace = Objects.toString(namespace, "");
        logName = Objects.toString(logName, "");

        if (StringUtils.isEmpty(ip)) {
            // traversing network interfaces if didn't specify an interface
            if (StringUtils.isEmpty(networkInterface)) {
                if (ipType == null) {
                    ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
                    String ipv6Addr = inetIPv6Utils.findIPv6Address();
                    metadata.put(IPV6, ipv6Addr);
                    if (ipv6Addr != null) {
                        metadata.put(IPV6, ipv6Addr);
                    }
                } else if (IPV4.equalsIgnoreCase(ipType)) {
                    ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
                } else if (IPV6.equalsIgnoreCase(ipType)) {
                    ip = inetIPv6Utils.findIPv6Address();
                    if (StringUtils.isEmpty(ip)) {
                        log.warn("There is no available IPv6 found. Spring Cloud Alibaba will automatically find IPv4.");
                        ip = inetUtils.findFirstNonLoopbackHostInfo().getIpAddress();
                    }
                } else {
                    throw new IllegalArgumentException("please checking the type of IP " + ipType);
                }
            } else {
                NetworkInterface netInterface = NetworkInterface.getByName(networkInterface);
                if (null == netInterface) {
                    throw new IllegalArgumentException("no such interface " + networkInterface);
                }

                Enumeration<InetAddress> inetAddress = netInterface.getInetAddresses();
                while (inetAddress.hasMoreElements()) {
                    InetAddress currentAddress = inetAddress.nextElement();
                    if (currentAddress instanceof Inet4Address || currentAddress instanceof Inet6Address && !currentAddress.isLoopbackAddress()) {
                        ip = currentAddress.getHostAddress();
                        break;
                    }
                }

                if (StringUtils.isEmpty(ip)) {
                    throw new RuntimeException("cannot find available ip from" + " network interface " + networkInterface);
                }

            }
        }

        this.overrideFromEnv(environment);
        if (nacosServiceManager.isNacosDiscoveryInfoChanged(this)) {
            applicationEventPublisher.publishEvent(new NacosDiscoveryInfoChangedEvent(this));
        }
        nacosServiceManager.setNacosDiscoveryProperties(this);
    }

    /**
     * recommend to use {@link NacosServiceManager#getNamingService()}.
     *
     * @return NamingService
     */
    @Deprecated
    public NamingService namingServiceInstance() {
        return nacosServiceManager.getNamingService();
    }

    public String getEndpoint() {
        return endpoint;
    }

    public void setEndpoint(String endpoint) {
        this.endpoint = endpoint;
    }

    public String getNamespace() {
        return namespace;
    }

    public void setNamespace(String namespace) {
        this.namespace = namespace;
    }

    public String getLogName() {
        return logName;
    }

    public void setLogName(String logName) {
        this.logName = logName;
    }

    public void setInetUtils(InetUtils inetUtils) {
        this.inetUtils = inetUtils;
    }

    public float getWeight() {
        return weight;
    }

    public void setWeight(float weight) {
        this.weight = weight;
    }

    public String getClusterName() {
        return clusterName;
    }

    public void setClusterName(String clusterName) {
        this.clusterName = clusterName;
    }

    public String getService() {
        return service;
    }

    public void setService(String service) {
        this.service = service;
    }

    public boolean isRegisterEnabled() {
        return registerEnabled;
    }

    public void setRegisterEnabled(boolean registerEnabled) {
        this.registerEnabled = registerEnabled;
    }

    public String getIp() {
        return ip;
    }

    public void setIp(String ip) {
        this.ip = ip;
    }

    public String getIpType() {
        return ipType;
    }

    public void setIpType(String ipType) {
        this.ipType = ipType;
    }

    public String getNetworkInterface() {
        return networkInterface;
    }

    public void setNetworkInterface(String networkInterface) {
        this.networkInterface = networkInterface;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public boolean isSecure() {
        return secure;
    }

    public void setSecure(boolean secure) {
        this.secure = secure;
    }

    public Map<String, String> getMetadata() {
        return metadata;
    }

    public void setMetadata(Map<String, String> metadata) {
        this.metadata = metadata;
    }

    public String getServerAddr() {
        return serverAddr;
    }

    public void setServerAddr(String serverAddr) {
        this.serverAddr = serverAddr;
    }

    public String getAccessKey() {
        return accessKey;
    }

    public void setAccessKey(String accessKey) {
        this.accessKey = accessKey;
    }

    public String getSecretKey() {
        return secretKey;
    }

    public void setSecretKey(String secretKey) {
        this.secretKey = secretKey;
    }

    public Integer getHeartBeatInterval() {
        return heartBeatInterval;
    }

    public void setHeartBeatInterval(Integer heartBeatInterval) {
        this.heartBeatInterval = heartBeatInterval;
    }

    public Integer getHeartBeatTimeout() {
        return heartBeatTimeout;
    }

    public void setHeartBeatTimeout(Integer heartBeatTimeout) {
        this.heartBeatTimeout = heartBeatTimeout;
    }

    public Integer getIpDeleteTimeout() {
        return ipDeleteTimeout;
    }

    public void setIpDeleteTimeout(Integer ipDeleteTimeout) {
        this.ipDeleteTimeout = ipDeleteTimeout;
    }

    public String getNamingLoadCacheAtStart() {
        return namingLoadCacheAtStart;
    }

    public void setNamingLoadCacheAtStart(String namingLoadCacheAtStart) {
        this.namingLoadCacheAtStart = namingLoadCacheAtStart;
    }

    public long getWatchDelay() {
        return watchDelay;
    }

    public void setWatchDelay(long watchDelay) {
        this.watchDelay = watchDelay;
    }

    public String getGroup() {
        return group;
    }

    public void setGroup(String group) {
        this.group = group;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public boolean isInstanceEnabled() {
        return instanceEnabled;
    }

    public void setInstanceEnabled(boolean instanceEnabled) {
        this.instanceEnabled = instanceEnabled;
    }

    public boolean isEphemeral() {
        return ephemeral;
    }

    public void setEphemeral(boolean ephemeral) {
        this.ephemeral = ephemeral;
    }

    public boolean isFailureToleranceEnabled() {
        return failureToleranceEnabled;
    }

    public void setFailureToleranceEnabled(boolean failureToleranceEnabled) {
        this.failureToleranceEnabled = failureToleranceEnabled;
    }

    public boolean isFailFast() {
        return failFast;
    }

    public void setFailFast(boolean failFast) {
        this.failFast = failFast;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        NacosDiscoveryProperties that = (NacosDiscoveryProperties) o;
        return watchDelay == that.watchDelay && Float.compare(that.weight, weight) == 0 && registerEnabled == that.registerEnabled && port == that.port && secure == that.secure && instanceEnabled == that.instanceEnabled && ephemeral == that.ephemeral && failureToleranceEnabled == that.failureToleranceEnabled && Objects.equals(serverAddr, that.serverAddr) && Objects.equals(username, that.username) && Objects.equals(password, that.password) && Objects.equals(endpoint, that.endpoint) && Objects.equals(namespace, that.namespace) && Objects.equals(logName, that.logName) && Objects.equals(service, that.service) && Objects.equals(clusterName, that.clusterName) && Objects.equals(group, that.group) && Objects.equals(namingLoadCacheAtStart, that.namingLoadCacheAtStart) && Objects.equals(metadata, that.metadata) && Objects.equals(ip, that.ip) && Objects.equals(networkInterface, that.networkInterface) && Objects.equals(accessKey, that.accessKey) && Objects.equals(secretKey, that.secretKey) && Objects.equals(heartBeatInterval, that.heartBeatInterval) && Objects.equals(heartBeatTimeout, that.heartBeatTimeout) && Objects.equals(failFast, that.failFast) && Objects.equals(ipDeleteTimeout, that.ipDeleteTimeout);
    }

    @Override
    public int hashCode() {
        return Objects.hash(serverAddr, username, password, endpoint, namespace, watchDelay, logName, service, weight, clusterName, group, namingLoadCacheAtStart, metadata, registerEnabled, ip,
                            networkInterface, port, secure, accessKey, secretKey, heartBeatInterval, heartBeatTimeout, ipDeleteTimeout, instanceEnabled, ephemeral, failureToleranceEnabled, failFast);
    }

    @Override
    public String toString() {
        return "NacosDiscoveryProperties{" + "serverAddr='" + serverAddr + '\'' + ", username='" + username + '\'' + ", password='" + password + '\'' + ", endpoint='" + endpoint + '\'' + ", " +
                "namespace='" + namespace + '\'' + ", watchDelay=" + watchDelay + ", logName='" + logName + '\'' + ", service='" + service + '\'' + ", weight=" + weight + ", clusterName='" + clusterName + '\'' + ", group='" + group + '\'' + ", namingLoadCacheAtStart='" + namingLoadCacheAtStart + '\'' + ", metadata=" + metadata + ", registerEnabled=" + registerEnabled + ", ip='" + ip + '\'' + ", networkInterface='" + networkInterface + '\'' + ", port=" + port + ", secure=" + secure + ", accessKey='" + accessKey + '\'' + ", secretKey='" + secretKey + '\'' + ", heartBeatInterval=" + heartBeatInterval + ", heartBeatTimeout=" + heartBeatTimeout + ", ipDeleteTimeout=" + ipDeleteTimeout + ", instanceEnabled=" + instanceEnabled + ", ephemeral=" + ephemeral + ", failureToleranceEnabled=" + failureToleranceEnabled + '}' + ", ipDeleteTimeout=" + ipDeleteTimeout + ", failFast=" + failFast + '}';
    }

    public void overrideFromEnv(Environment env) {

        if (StringUtils.isEmpty(this.getServerAddr())) {
            String serverAddr = env.resolvePlaceholders("${spring.cloud.nacos.discovery.server-addr:}");
            if (StringUtils.isEmpty(serverAddr)) {
                serverAddr = env.resolvePlaceholders("${spring.cloud.nacos.server-addr:192.168.1.237:8848}");
            }
            this.setServerAddr(serverAddr);
        }
        if (StringUtils.isEmpty(this.getNamespace())) {
            this.setNamespace(env.resolvePlaceholders("${spring.cloud.nacos.discovery.namespace:}"));
        }
        if (StringUtils.isEmpty(this.getAccessKey())) {
            this.setAccessKey(env.resolvePlaceholders("${spring.cloud.nacos.discovery.access-key:}"));
        }
        if (StringUtils.isEmpty(this.getSecretKey())) {
            this.setSecretKey(env.resolvePlaceholders("${spring.cloud.nacos.discovery.secret-key:}"));
        }
        if (StringUtils.isEmpty(this.getLogName())) {
            this.setLogName(env.resolvePlaceholders("${spring.cloud.nacos.discovery.log-name:}"));
        }
        if (StringUtils.isEmpty(this.getClusterName())) {
            this.setClusterName(env.resolvePlaceholders("${spring.cloud.nacos.discovery.cluster-name:}"));
        }
        if (StringUtils.isEmpty(this.getEndpoint())) {
            this.setEndpoint(env.resolvePlaceholders("${spring.cloud.nacos.discovery.endpoint:}"));
        }
        if (StringUtils.isEmpty(this.getGroup())) {
            this.setGroup(env.resolvePlaceholders("${spring.cloud.nacos.discovery.group:}"));
        }
        if (StringUtils.isEmpty(this.getUsername())) {
            this.setUsername(env.resolvePlaceholders("${spring.cloud.nacos.username:}"));
        }
        if (StringUtils.isEmpty(this.getPassword())) {
            this.setPassword(env.resolvePlaceholders("${spring.cloud.nacos.password:}"));
        }
    }

    public Properties getNacosProperties() {
        Properties properties = new Properties();
        properties.put(SERVER_ADDR, serverAddr);
        properties.put(USERNAME, Objects.toString(username, ""));
        properties.put(PASSWORD, Objects.toString(password, ""));
        properties.put(NAMESPACE, namespace);
        properties.put(UtilAndComs.NACOS_NAMING_LOG_NAME, logName);

        if (endpoint.contains(":")) {
            int index = endpoint.indexOf(":");
            properties.put(ENDPOINT, endpoint.substring(0, index));
            properties.put(ENDPOINT_PORT, endpoint.substring(index + 1));
        } else {
            properties.put(ENDPOINT, endpoint);
        }

        properties.put(ACCESS_KEY, accessKey);
        properties.put(SECRET_KEY, secretKey);
        // only used for instance.setClusterName()
        //		properties.put(CLUSTER_NAME, clusterName);
        properties.put(NAMING_LOAD_CACHE_AT_START, namingLoadCacheAtStart);

        enrichNacosDiscoveryProperties(properties);
        return properties;
    }

    private void enrichNacosDiscoveryProperties(Properties nacosDiscoveryProperties) {
        Map<String, Object> properties = PropertySourcesUtils.getSubProperties((ConfigurableEnvironment) environment, PREFIX);
        properties.forEach((k, v) -> nacosDiscoveryProperties.putIfAbsent(resolveKey(k), String.valueOf(v)));
    }

    private String resolveKey(String key) {
        Matcher matcher = PATTERN.matcher(key);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            matcher.appendReplacement(sb, matcher.group(1).toUpperCase());
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

}
